package com.amadeus.spin.devtool.soap.shooter.validate;

import java.io.FileNotFoundException;

import javax.xml.transform.TransformerException;

import org.apache.log4j.Logger;
import org.apache.xpath.XPathAPI;
import org.w3c.dom.Document;
import org.w3c.dom.NamedNodeMap;
import org.w3c.dom.Node;
import org.w3c.dom.NodeList;

import com.amadeus.spin.devtool.soap.shooter.FatalException;
import com.amadeus.spin.devtool.soap.shooter.ProcessOutput;
import com.amadeus.spin.devtool.soap.shooter.config.ConfigurationParameters;
import com.amadeus.spin.standard.util.Dom4jUtils;
import com.amadeus.spin.standard.util.XMLTester;
import com.amadeus.spin.util.soapshooter.Shooter;

public class ValidateOutput implements ProcessOutput {

  private static final String TRUE = "true";

  private static Logger logger = Logger.getLogger(ValidateOutput.class);

  protected Document m_document;

  protected XMLTester m_tester;

  private enum TestNodeType {
    root("testRoot"), exists("testExists"), notExists("testNotExists"), notExistsWithValue("testNotExistsWithValue"),
    value("testValue"), regexp("testRegexp"), printValue("printValue");
    String name;

    TestNodeType(String tagName) {
      this.name = tagName;
    }

    public static TestNodeType eval(String tagName) {
      for (TestNodeType t : values()) {
        if (t.name.equals(tagName)) {
          return t;
        }
      }
      return null;
    }
  };

  public ValidateOutput() {
    m_tester = new XMLTester();
  }

  @Override
  public void processOutput(String xmlOutput, Node config, Shooter shooter) throws FatalException {

    // ListActions tag is only required when no validation import has been set
    boolean isListActionsRequired = true;
    Node validationImportNode = initialize(xmlOutput, config, "ValidationImport", false);

    if (validationImportNode != null) {
      isListActionsRequired = false;
      loadValidationFile(xmlOutput, validationImportNode, shooter);
    }

    Node listActionNode = initialize(xmlOutput, config, "ListActions", isListActionsRequired);
    testListActionNodes(listActionNode);
  }

  public final void testListActionNodes(Node listActionNode) throws FatalException {
    NodeList testList = listActionNode.getChildNodes();
    Node testNode = null;
    for (int i = 0; i < testList.getLength(); ++i) {
      testNode = testList.item(i);
      short type = testNode.getNodeType();
      if (type == Node.ELEMENT_NODE) {
        TestNodeType enumVal = TestNodeType.eval(testNode.getNodeName());
        if (enumVal == null) {
          logger.error("found unknown test node: " + testNode.getNodeName());
          return;
        }
        switch (enumVal) {
        case root:
          testRoot(testNode);
          break;
        case exists:
          testExists(testNode);
          break;
        case notExists:
          testNotExists(testNode);
          break;
        case notExistsWithValue:
          testNotExistsWithValue(testNode);
          break;
        case value:
          testValue(testNode);
          break;
        case regexp:
          testRegexp(testNode);
          break;
        case printValue:
          printValue(testNode);
          break;
        default:
          break;
        }
      }
    }
  }

  /**
   * Replaces all the variables of the type $myVariable by its assigned value
   *
   * @param nodeContent
   */
  protected String replaceVariables(String nodeContent) {
    return com.amadeus.spin.devtool.soap.shooter.Shooter.replaceVariables(nodeContent);
  }

  public final Node initialize(String xmlOutput, Node config, String nodeName, boolean isRequired)
      throws FatalException {
    Node actionNode = null;
    try {
      actionNode = getListActionNode(config, nodeName);
      if (actionNode == null && isRequired) {
        logger.warn("No <" + nodeName + "> defined under <ValidateOutput> in your scenario.");
      }
    }
    catch (FatalException e) {
      if (isRequired) {
        logger.error("Error during Output functional validation.", e);
        throw e;
      }
    }
    this.m_document = Dom4jUtils.getDocumentFromString(xmlOutput);
    this.m_tester.setDOM(this.m_document);
    return actionNode;
  }

  protected void testRoot(Node testNode) throws FatalException {
    ValidateTestNode node = new ValidateTestNode(testNode, true, false, "testRoot");
    boolean isOK = this.m_tester.testRoot(node.value);
    checkResult(node.fatal, isOK, "testRoot(" + node.value + ")");
  }

  /**
   * test the value of a node
   *
   * @param testNode
   * @param encoding
   * @throws FatalException
   */
  protected void testValue(Node testNode) throws FatalException {
    ValidateTestNode node = new ValidateTestNode(testNode, true, true, "testValue");
    String xpath = node.node;
    String value = node.value;
    boolean isOK = this.m_tester.testValue(xpath, value);
    String call = "testValue(" + xpath + "," + value + ")";
    checkResult(node.fatal, isOK, call);
  }

  private void checkResult(boolean fatal, boolean isOK, String call) throws FatalException {
    if (isOK) {
      logger.info("OK :: " + call);
      return;
    }
    String str = "KO :: " + call + ".\nFound \"" + this.m_tester.getWrongValue() + "\"";
    reportFailure(str, fatal);
  }

  /**
   * test that a node exists
   *
   * @param testNode
   * @throws FatalException
   */
  private void testNotExistsWithValue(Node testNode) throws FatalException {
    ValidateTestNode node = new ValidateTestNode(testNode, true, true, "testNotExistsWithValue");
    String xpath = node.node;
    String value = node.value;
    boolean isOK = this.m_tester.testNotExistsWithValue(xpath, value);
    String call = "testNotExistsWithValue(" + xpath + "," + value + ")";
    if (isOK) {
      logger.info("OK :: " + call);
      return;
    }
    reportFailure("KO :: " + call + ".\nFound \"" + value + "\"", node.fatal);
  }

  private void printValue(Node testNode) throws FatalException {
    ValidateTestNode node = new ValidateTestNode(testNode, false, true, "printValue");
    String strXPath = node.node;
    String strReturnedValue = this.m_tester.getXPathString(strXPath);
    if (strReturnedValue == null) {
      strReturnedValue = "/!\\ Value not found /!\\";
    }
    logger.info("printValue(" + strXPath + ") = " + strReturnedValue);
  }

  private void testExists(Node testNode) throws FatalException {
    ValidateTestNode node = new ValidateTestNode(testNode, false, true, "testExists");
    boolean isOK;
    String strXPath = node.node;
    isOK = this.m_tester.testExists(strXPath);

    String call = "testExists(" + strXPath + ")";
    if (!(isOK)) {
      String errorStr = "KO :: " + call + ".\nNode does not exist.";
      reportFailure(errorStr, node.fatal);
    }
    else {
      logger.info("OK :: " + call);
      if (node.printValue) {
        try {
          String value = XPathAPI.eval(this.m_document, strXPath).toString();
          logger.info(call + " --> value found is '" + value + "'.");
        }
        catch (TransformerException e) {
          throw makeInvalidXpathException(strXPath, e);
        }
      }
    }
  }

  /**
   * Throws a fatal exception
   *
   * @param strXPath
   * @param e
   * @return
   */
  private FatalException makeInvalidXpathException(String strXPath, TransformerException e) {
    String msg = "Error in the xpath specifiied in condition - please check your scenario " +
        "(probably a useless trailing '/' in '" + strXPath + "')";
    logger.error(msg);
    return new FatalException(msg, e);
  }

  /**
   * Calls the notExists validator
   *
   * @param testNode
   * @throws FatalException
   */
  private void testNotExists(Node testNode) throws FatalException {
    ValidateTestNode node = new ValidateTestNode(testNode, false, true, "testNotExists");
    String strXPath = node.node;
    boolean isOK = this.m_tester.testNotExists(strXPath);
    String call = "testNotExists(" + strXPath + ")";
    if (isOK) {
      logger.info("OK :: " + call);
      return;
    }
    String str = "KO :: " + call + ".\nA node was found.";
    reportFailure(str, node.fatal);
  }

  private void testRegexp(Node testNode) throws FatalException {
    ValidateTestNode node = new ValidateTestNode(testNode, true, true, "testRegexp");
    String strXPath = node.node;
    String strValue = node.value;
    boolean isOK = this.m_tester.testRegexp(strXPath, strValue);
    String call = "testRegexp(" + strXPath + "," + strValue + ")";
    if (isOK) {
      logger.info("OK :: " + call + " [" + this.m_tester.getWrongValue() + "]");
      return;
    }
    String str = "KO :: " + call + ".\nFound \"" + this.m_tester.getWrongValue() + "\"";
    reportFailure(str, node.fatal);
  }

  /**
   * Retrieves the direct child of the config named nodeName
   *
   * @param configNode
   * @param nodeName
   * @return
   * @throws FatalException
   */
  public Node getListActionNode(Node configNode, String nodeName) throws FatalException {
    NodeList list;
    try {
      list = configNode.getChildNodes();
      for (int i = 0; i < list.getLength(); ++i) {
        Node node = list.item(i);
        if (node.getNodeType() == Node.ELEMENT_NODE && node.getNodeName().equals(nodeName)) {
          return node;
        }
      }
      return null;
    }
    catch (Exception e) {
      String msg = "cannot find node " + nodeName;
      logger.error(msg);
      throw new FatalException(msg, e);
    }
  }

  /**
   * Formats the error correctly
   *
   * @param msg
   * @param strFatal
   * @throws FatalException
   */
  private void reportFailure(String msg, boolean fatal) throws FatalException {
    if (fatal) {
      logger.error(msg);
    }
    else {
      logger.warn(msg);
    }
    if (fatal) {
      logger.error("Fatal Error. (fatal parameter = true for this test)");
      throw new FatalException(msg);
    }
  }

  /**
   * Loads an validation file that respects the same format as the inside of the ValidateOutput tag
   *
   * @param xmlOutput
   * @param testNode
   * @param shooter
   * @throws FatalException
   */
  private void loadValidationFile(String xmlOutput, Node testNode, Shooter shooter) throws FatalException {
    NamedNodeMap attributes = testNode.getAttributes();
    Node input = attributes.getNamedItem("input");
    if (input == null) {
      String msg = "validationImport node has incorrect format";
      logger.error(msg);
      throw new FatalException(msg);
    }
    String strInput = input.getNodeValue();

    if (strInput != null && !strInput.equals("")) {
      String scenarioFileName = ConfigurationParameters.getScenarioFileName().replaceAll("\\\\", "/");
      String scenarioDir = scenarioFileName.substring(0, scenarioFileName.lastIndexOf('/'));

      String fullPath = scenarioDir + '/' + strInput;

      try {
        logger.info("Loading validation import file " + fullPath);
        processOutput(xmlOutput, Dom4jUtils.getDocumentFromFile(fullPath), shooter);
      }
      catch (FileNotFoundException e) {
        String msg = "Referenced file in <ValidationImport> was not found at " + fullPath;
        logger.error(msg);
        throw new FatalException(msg);
      }
      catch (FatalException e) {
        // Throw validation exception
        throw e;
      }
      catch (Exception e) {
        String msg = "Failed to parse validation file " + strInput + ". Does it respect the xsd ?";
        logger.error(msg);
        throw new FatalException(msg);
      }

    }
    else {
      String msg = "input parameter for validationImport is mandatory";
      logger.error(msg);
      throw new FatalException(msg);
    }

  }

  /**
   * Private class because no one else is supposed to access this object type
   *
   */
  private class ValidateTestNode {
    private String node;

    private String value;

    private boolean fatal;

    private boolean printValue;

    ValidateTestNode(Node testNode, boolean isValueMandatory, boolean isNodeMandatory, String method)
        throws FatalException {
      NamedNodeMap attributes = testNode.getAttributes();
      Node node = attributes.getNamedItem("node");
      Node value = attributes.getNamedItem("value");
      Node fatal = attributes.getNamedItem("fatal");
      Node print = attributes.getNamedItem("printvalue");
      if ((isValueMandatory && value == null) || (isNodeMandatory && node == null)) {
        String msg = method + " node has incorrect format";
        logger.error(msg);
        throw new FatalException(msg);
      }
      if (value != null) {
        this.value = replaceVariables(value.getNodeValue());
      }
      if (fatal != null) {
        this.fatal = TRUE.equals(fatal.getNodeValue());
      }
      if (print != null) {
        this.printValue = TRUE.equals(print.getNodeValue());
      }
      if (node != null) {
        this.node = replaceVariables(node.getNodeValue());
      }
    }
  }
}